package com.thaiopensource.relaxng.output.dtd;

import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import com.thaiopensource.relaxng.edit.AbstractPatternVisitor;
import com.thaiopensource.relaxng.edit.AbstractVisitor;
import com.thaiopensource.relaxng.edit.Annotated;
import com.thaiopensource.relaxng.edit.AnnotationChild;
import com.thaiopensource.relaxng.edit.AttributeAnnotation;
import com.thaiopensource.relaxng.edit.AttributePattern;
import com.thaiopensource.relaxng.edit.ChoiceNameClass;
import com.thaiopensource.relaxng.edit.ChoicePattern;
import com.thaiopensource.relaxng.edit.Comment;
import com.thaiopensource.relaxng.edit.Component;
import com.thaiopensource.relaxng.edit.ComponentVisitor;
import com.thaiopensource.relaxng.edit.CompositePattern;
import com.thaiopensource.relaxng.edit.Container;
import com.thaiopensource.relaxng.edit.DataPattern;
import com.thaiopensource.relaxng.edit.DefineComponent;
import com.thaiopensource.relaxng.edit.DivComponent;
import com.thaiopensource.relaxng.edit.ElementPattern;
import com.thaiopensource.relaxng.edit.GrammarPattern;
import com.thaiopensource.relaxng.edit.GroupPattern;
import com.thaiopensource.relaxng.edit.IncludeComponent;
import com.thaiopensource.relaxng.edit.InterleavePattern;
import com.thaiopensource.relaxng.edit.ListPattern;
import com.thaiopensource.relaxng.edit.MixedPattern;
import com.thaiopensource.relaxng.edit.NameClass;
import com.thaiopensource.relaxng.edit.NameNameClass;
import com.thaiopensource.relaxng.edit.OneOrMorePattern;
import com.thaiopensource.relaxng.edit.OptionalPattern;
import com.thaiopensource.relaxng.edit.Param;
import com.thaiopensource.relaxng.edit.Pattern;
import com.thaiopensource.relaxng.edit.PatternVisitor;
import com.thaiopensource.relaxng.edit.RefPattern;
import com.thaiopensource.relaxng.edit.TextPattern;
import com.thaiopensource.relaxng.edit.ValuePattern;
import com.thaiopensource.relaxng.edit.ZeroOrMorePattern;
import com.thaiopensource.relaxng.output.OutputDirectory;
import com.thaiopensource.relaxng.output.common.ErrorReporter;
import com.thaiopensource.relaxng.output.common.NameClassSplitter;
import com.thaiopensource.util.VoidValue;
import com.thaiopensource.xml.out.CharRepertoire;
import com.thaiopensource.xml.util.Naming;
import com.thaiopensource.xml.util.WellKnownNamespaces;

class DtdOutput
{
  private final boolean warnDatatypes;
  private final String sourceUri;
  private Writer writer;
  private String encoding;
  private CharRepertoire charRepertoire;
  private final int indent;
  private final int lineLength;
  private final String lineSep;
  private final StringBuffer buf = new StringBuffer ();
  private final List <ElementPattern> elementQueue = new Vector <ElementPattern> ();
  private final List <String> requiredParamEntities = new Vector <String> ();
  private final List <String> externallyRequiredParamEntities = new Vector <String> ();
  private final Set <String> doneParamEntities = new HashSet <String> ();
  private final Set <String> doneIncludes = new HashSet <String> ();
  private final Set <String> pendingIncludes = new HashSet <String> ();
  private final Analysis analysis;
  private final GrammarPart part;
  private final OutputDirectory od;
  private final ErrorReporter er;
  private final Set <String> reservedEntityNames;

  private final PatternVisitor <VoidValue> topLevelContentModelOutput = new TopLevelContentModelOutput ();
  private final AbstractVisitor nestedContentModelOutput = new ContentModelOutput ();
  private final PatternVisitor <VoidValue> expandedContentModelOutput = new ExpandedContentModelOutput ();
  private final PatternVisitor <VoidValue> groupContentModelOutput = new GroupContentModelOutput ();
  private final PatternVisitor <VoidValue> choiceContentModelOutput = new ChoiceContentModelOutput ();
  private final PatternVisitor <VoidValue> occurContentModelOutput = new ParenthesizedContentModelOutput ();
  private final PatternVisitor <VoidValue> innerElementClassOutput = new InnerElementClassOutput ();
  private final PatternVisitor <VoidValue> expandedInnerElementClassOutput = new ExpandedInnerElementClassOutput ();
  private final AttributeOutput attributeOutput = new AttributeOutput ();
  private final AttributeOutput optionalAttributeOutput = new OptionalAttributeOutput ();
  private final PatternVisitor <VoidValue> topLevelSimpleTypeOutput = new TopLevelSimpleTypeOutput ();
  private final PatternVisitor <VoidValue> nestedSimpleTypeOutput = new SimpleTypeOutput ();
  private final PatternVisitor <VoidValue> valueOutput = new ValueOutput ();
  private final GrammarOutput grammarOutput = new GrammarOutput ();

  static private final String DTD_URI = "http://www.thaiopensource.com/ns/relaxng/dtd";

  private DtdOutput (final boolean warnDatatypes,
                     final String sourceUri,
                     final Analysis analysis,
                     final Set <String> reservedEntityNames,
                     final OutputDirectory od,
                     final ErrorReporter er)
  {
    this.warnDatatypes = warnDatatypes;
    this.sourceUri = sourceUri;
    this.analysis = analysis;
    this.reservedEntityNames = reservedEntityNames;
    this.od = od;
    this.er = er;
    this.part = analysis.getGrammarPart (sourceUri);
    try
    {
      final OutputDirectory.Stream stream = od.open (sourceUri, analysis.getEncoding (sourceUri));
      this.encoding = stream.getEncoding ();
      this.writer = stream.getWriter ();
      this.charRepertoire = stream.getCharRepertoire ();
    }
    catch (final IOException e)
    {
      throw new WrappedIOException (e);
    }
    this.lineSep = od.getLineSeparator ();
    this.indent = od.getIndent ();
    this.lineLength = od.getLineLength ();
  }

  class ParenthesizedContentModelOutput extends AbstractPatternVisitor <VoidValue>
  {
    @Override
    public VoidValue visitPattern (final Pattern p)
    {
      buf.append ('(');
      p.accept (nestedContentModelOutput);
      buf.append (')');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      final Pattern def = getBody (p.getName ());
      if (getContentType (def) == ContentType.DIRECT_SINGLE_ELEMENT)
        ((ElementPattern) def).getNameClass ().accept (nestedContentModelOutput);
      else
        visitPattern (p);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitElement (final ElementPattern p)
    {
      if (getContentType (p) == ContentType.DIRECT_SINGLE_ELEMENT)
      {
        p.getNameClass ().accept (nestedContentModelOutput);
        elementQueue.add (p);
      }
      else
        visitPattern (p);
      return VoidValue.VOID;
    }
  }

  class ChoiceContentModelOutput extends ParenthesizedContentModelOutput
  {
    @Override
    public VoidValue visitOptional (final OptionalPattern p)
    {
      p.accept (nestedContentModelOutput);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOneOrMore (final OneOrMorePattern p)
    {
      p.accept (nestedContentModelOutput);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      p.accept (nestedContentModelOutput);
      return VoidValue.VOID;
    }
  }

  private class GroupContentModelOutput extends ChoiceContentModelOutput
  {
    @Override
    public VoidValue visitGroup (final GroupPattern p)
    {
      p.accept (nestedContentModelOutput);
      return VoidValue.VOID;
    }
  }

  class ContentModelOutput extends AbstractVisitor
  {
    @Override
    public VoidValue visitName (final NameNameClass nc)
    {
      final String prefix = analysis.getElementPrefixForNamespaceUri (nc.getNamespaceUri ());
      if (prefix != null)
        buf.append (prefix).append (':');
      buf.append (nc.getLocalName ());
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitChoice (final ChoiceNameClass nc)
    {
      final List <NameClass> list = nc.getChildren ();
      boolean needSep = false;
      for (int i = 0, len = list.size (); i < len; i++)
      {
        if (needSep)
          buf.append ('|');
        else
          needSep = true;
        list.get (i).accept (this);
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitElement (final ElementPattern p)
    {
      p.getNameClass ().accept (this);
      elementQueue.add (p);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      final Pattern def = getBody (p.getName ());
      if (getContentType (def) == ContentType.DIRECT_SINGLE_ELEMENT)
        ((ElementPattern) def).getNameClass ().accept (this);
      else
        paramEntityRef (p);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      p.getChild ().accept (occurContentModelOutput);
      buf.append ('*');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOneOrMore (final OneOrMorePattern p)
    {
      p.getChild ().accept (occurContentModelOutput);
      buf.append ('+');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOptional (final OptionalPattern p)
    {
      p.getChild ().accept (occurContentModelOutput);
      buf.append ('?');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitText (final TextPattern p)
    {
      buf.append ("#PCDATA");
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitMixed (final MixedPattern p)
    {
      buf.append ("#PCDATA");
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitGroup (final GroupPattern p)
    {
      final List <Pattern> list = p.getChildren ();
      boolean needSep = false;
      final int len = list.size ();
      for (int i = 0; i < len; i++)
      {
        final Pattern member = list.get (i);
        final ContentType t = getContentType (member);
        if (!t.isA (ContentType.EMPTY))
        {
          if (needSep)
            buf.append (',');
          else
            needSep = true;
          member.accept (groupContentModelOutput);
        }
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitInterleave (final InterleavePattern p)
    {
      final ContentType ct = getContentType (p);
      if (ct == ContentType.INTERLEAVE_ZERO_OR_MORE_ELEMENT_CLASS || ct == ContentType.INTERLEAVE_MIXED_MODEL)
      {
        buf.append ('(');
        p.accept (innerElementClassOutput);
        buf.append (')');
        buf.append ('*');
      }
      else
      {
        final List <Pattern> list = p.getChildren ();
        for (int i = 0, len = list.size (); i < len; i++)
        {
          final Pattern member = list.get (i);
          final ContentType t = getContentType (member);
          if (!t.isA (ContentType.EMPTY))
            member.accept (this);
        }
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitChoice (final ChoicePattern p)
    {
      final List <Pattern> list = p.getChildren ();
      boolean needSep = false;
      final int len = list.size ();
      if (getContentType (p).isA (ContentType.MIXED_ELEMENT_CLASS))
      {
        for (int i = 0; i < len; i++)
        {
          final Pattern member = list.get (i);
          if (getContentType (member).isA (ContentType.MIXED_ELEMENT_CLASS))
          {
            member.accept (nestedContentModelOutput);
            needSep = true;
            break;
          }
        }
      }
      for (int i = 0; i < len; i++)
      {
        final Pattern member = list.get (i);
        final ContentType t = getContentType (member);
        if (t != ContentType.NOT_ALLOWED && t != ContentType.EMPTY && !t.isA (ContentType.MIXED_ELEMENT_CLASS))
        {
          if (needSep)
            buf.append ('|');
          else
            needSep = true;
          member.accept (!t.isA (ContentType.ELEMENT_CLASS) ? choiceContentModelOutput : nestedContentModelOutput);
        }
      }
      for (int i = 0; i < len; i++)
      {
        final Pattern member = list.get (i);
        final ContentType t = getContentType (member);
        if (t == ContentType.NOT_ALLOWED)
        {
          if (needSep)
            buf.append (' ');
          else
            needSep = true;
          member.accept (nestedContentModelOutput);
        }
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitGrammar (final GrammarPattern p)
    {
      return getBody (DefineComponent.START).accept (this);
    }
  }

  class TopLevelContentModelOutput extends ContentModelOutput
  {
    @Override
    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      buf.append ('(');
      p.getChild ().accept (nestedContentModelOutput);
      buf.append (')');
      buf.append ('*');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOneOrMore (final OneOrMorePattern p)
    {
      buf.append ('(');
      p.getChild ().accept (nestedContentModelOutput);
      buf.append (')');
      buf.append ('+');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOptional (final OptionalPattern p)
    {
      buf.append ('(');
      p.getChild ().accept (nestedContentModelOutput);
      buf.append (')');
      buf.append ('?');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitElement (final ElementPattern p)
    {
      buf.append ('(');
      super.visitElement (p);
      buf.append (')');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      final ContentType t = getContentType (p);
      if (t.isA (ContentType.MIXED_MODEL))
        super.visitRef (p);
      else
      {
        buf.append ('(');
        super.visitRef (p);
        buf.append (')');
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitChoice (final ChoicePattern p)
    {
      buf.append ('(');
      p.accept (nestedContentModelOutput);
      buf.append (')');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitText (final TextPattern p)
    {
      buf.append ('(');
      p.accept (nestedContentModelOutput);
      buf.append (')');
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitMixed (final MixedPattern p)
    {
      buf.append ('(');
      if (getContentType (p.getChild ()) == ContentType.EMPTY)
        buf.append ("#PCDATA)");
      else
      {
        buf.append ("#PCDATA|");
        p.getChild ().accept (innerElementClassOutput);
        buf.append (')');
        buf.append ('*');
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitGroup (final GroupPattern p)
    {
      final List <Pattern> list = p.getChildren ();
      Pattern main = null;
      for (int i = 0, len = list.size (); i < len; i++)
      {
        final Pattern member = list.get (i);
        if (!getContentType (member).isA (ContentType.EMPTY))
        {
          if (main == null)
            main = member;
          else
          {
            buf.append ('(');
            nestedContentModelOutput.visitGroup (p);
            buf.append (')');
            return VoidValue.VOID;
          }
        }
      }
      if (main != null)
        main.accept (this);
      return VoidValue.VOID;
    }
  }

  class ExpandedContentModelOutput extends ContentModelOutput
  {
    @Override
    public VoidValue visitElement (final ElementPattern p)
    {
      p.getNameClass ().accept (this);
      return VoidValue.VOID;
    }
  }

  class PatternOutput extends AbstractPatternVisitor <VoidValue>
  {
    @Override
    public VoidValue visitPattern (final Pattern p)
    {
      return VoidValue.VOID;
    }
  }

  class InnerElementClassOutput extends PatternOutput
  {
    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      getBody (p.getName ()).accept (expandedInnerElementClassOutput);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitComposite (final CompositePattern p)
    {
      final List <Pattern> list = p.getChildren ();
      boolean needSep = false;
      int doneIndex = -1;
      for (int i = 0, len = list.size (); i < len; i++)
      {
        final Pattern member = list.get (i);
        final ContentType ct = getContentType (member);
        if (ct.isA (ContentType.MIXED_MODEL) || ct == ContentType.TEXT)
        {
          member.accept (this);
          needSep = true;
          doneIndex = i;
          break;
        }
      }
      for (int i = 0, len = list.size (); i < len; i++)
      {
        if (i != doneIndex)
        {
          final Pattern member = list.get (i);
          if (getContentType (member) != ContentType.EMPTY)
          {
            if (needSep)
              buf.append ('|');
            else
              needSep = true;
            member.accept (this);
          }
        }
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      p.getChild ().accept (nestedContentModelOutput);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitMixed (final MixedPattern p)
    {
      if (getContentType (p.getChild ()) == ContentType.EMPTY)
        buf.append ("#PCDATA");
      else
      {
        buf.append ("#PCDATA|");
        p.getChild ().accept (this);
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitText (final TextPattern p)
    {
      buf.append ("#PCDATA");
      return VoidValue.VOID;
    }
  }

  class ExpandedInnerElementClassOutput extends InnerElementClassOutput
  {
    @Override
    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      p.getChild ().accept (expandedContentModelOutput);
      return VoidValue.VOID;
    }
  }

  class AttributeOutput extends PatternOutput
  {
    void output (final Pattern p)
    {
      if (getAttributeType (p) != AttributeType.EMPTY)
        p.accept (this);
    }

    void newlineIndent ()
    {
      buf.append (lineSep);
      for (int i = 0; i < indent; i++)
        buf.append (' ');
    }

    @Override
    public VoidValue visitComposite (final CompositePattern p)
    {
      final List <Pattern> list = p.getChildren ();
      for (int i = 0, len = list.size (); i < len; i++)
        output (list.get (i));
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitMixed (final MixedPattern p)
    {
      output (p.getChild ());
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOneOrMore (final OneOrMorePattern p)
    {
      output (p.getChild ());
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      if (getAttributeType (p) != AttributeType.SINGLE)
        er.warning ("attribute_occur_approx", p.getSourceLocation ());
      optionalAttributeOutput.output (p.getChild ());
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      final ContentType t = getContentType (p);
      if (t.isA (ContentType.EMPTY) && isRequired ())
      {
        if (analysis.getParamEntityElementName (p.getName ()) == null)
        {
          newlineIndent ();
          paramEntityRef (p);
        }
      }
      else
        output (getBody (p.getName ()));
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitAttribute (final AttributePattern p)
    {
      final ContentType ct = getContentType (p.getChild ());
      if (ct == ContentType.NOT_ALLOWED)
        return VoidValue.VOID;
      final List <NameNameClass> names = NameClassSplitter.split (p.getNameClass ());
      final int len = names.size ();
      if (len > 1)
        er.warning ("attribute_occur_approx", p.getSourceLocation ());
      for (int i = 0; i < len; i++)
      {
        final int start = buf.length ();
        newlineIndent ();
        final NameNameClass nnc = names.get (i);
        final String ns = nnc.getNamespaceUri ();

        if (!ns.equals ("") && ns != NameClass.INHERIT_NS)
        {
          final String prefix = analysis.getPrefixForNamespaceUri (ns);
          buf.append (prefix);
          buf.append (':');
        }
        buf.append (nnc.getLocalName ());
        buf.append (' ');
        if (ct == ContentType.VALUE)
          p.getChild ().accept (valueOutput);
        else
        {
          final int typeStart = buf.length ();
          if (ct.isA (ContentType.SIMPLE_TYPE) || ct == ContentType.TEXT)
            p.getChild ().accept (topLevelSimpleTypeOutput);
          else
            if (ct == ContentType.EMPTY)
            {
              er.warning ("empty_attribute_approx", p.getSourceLocation ());
              buf.append ("CDATA");
            }
          final int typeEnd = buf.length ();
          if (isRequired () && len == 1)
            buf.append (" #REQUIRED");
          else
          {
            final String dv = getDefaultValue (p);
            if (dv == null)
              buf.append (" #IMPLIED");
            else
            {
              buf.append (' ');
              attributeValueLiteral (dv);
            }
          }
          final int lineStart = start + lineSep.length ();
          if (buf.length () - lineStart > lineLength && ct.isA (ContentType.ENUM))
          {
            final ModelBreaker breaker = new ModelBreaker (buf.substring (lineStart, typeStart),
                                                           buf.substring (typeStart, typeEnd),
                                                           buf.substring (typeEnd),
                                                           lineLength);
            buf.setLength (start);
            while (breaker.hasNextLine ())
            {
              buf.append (lineSep);
              buf.append (breaker.nextLine ());
            }
          }
        }
      }
      return VoidValue.VOID;
    }

    boolean isRequired ()
    {
      return true;
    }

    @Override
    public VoidValue visitChoice (final ChoicePattern p)
    {
      if (getAttributeType (p) != AttributeType.EMPTY)
        er.warning ("attribute_occur_approx", p.getSourceLocation ());
      optionalAttributeOutput.visitComposite (p);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitOptional (final OptionalPattern p)
    {
      if (getAttributeType (p) != AttributeType.SINGLE)
        er.warning ("attribute_occur_approx", p.getSourceLocation ());
      optionalAttributeOutput.output (p.getChild ());
      return VoidValue.VOID;
    }
  }

  class OptionalAttributeOutput extends AttributeOutput
  {
    @Override
    boolean isRequired ()
    {
      return false;
    }
  }

  class SimpleTypeOutput extends PatternOutput
  {
    @Override
    public VoidValue visitText (final TextPattern p)
    {
      buf.append ("CDATA");
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitValue (final ValuePattern p)
    {
      buf.append (p.getValue ());
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      paramEntityRef (p);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitData (final DataPattern p)
    {
      final Datatypes.Info info = Datatypes.getInfo (p.getDatatypeLibrary (), p.getType ());
      if (info == null)
      {
        er.warning ("unrecognized_datatype", p.getSourceLocation ());
        buf.append ("CDATA");
      }
      else
      {
        if (warnDatatypes)
        {
          if (!info.isExact ())
            er.warning ("datatype_approx", p.getType (), info.closestType (), p.getSourceLocation ());
          else
          {
            for (final Param param : p.getParams ())
              er.warning ("ignore_param", param.getName (), p.getType (), p.getSourceLocation ());
            if (p.getExcept () != null)
              er.warning ("ignore_except", p.getType (), p.getSourceLocation ());
          }
        }
        buf.append (info.closestType ());
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitChoice (final ChoicePattern p)
    {
      final List <Pattern> list = p.getChildren ();
      boolean needSep = false;
      final int len = list.size ();
      for (int i = 0; i < len; i++)
      {
        final Pattern member = list.get (i);
        final ContentType t = getContentType (member);
        if (t != ContentType.NOT_ALLOWED)
        {
          if (needSep)
            buf.append ('|');
          else
            needSep = true;
          member.accept (this);
        }
      }
      for (int i = 0; i < len; i++)
      {
        final Pattern member = list.get (i);
        final ContentType t = getContentType (member);
        if (t == ContentType.NOT_ALLOWED)
        {
          if (needSep)
            buf.append (' ');
          else
            needSep = true;
          member.accept (this);
        }
      }
      return VoidValue.VOID;
    }
  }

  class TopLevelSimpleTypeOutput extends SimpleTypeOutput
  {
    @Override
    public VoidValue visitList (final ListPattern p)
    {
      if (warnDatatypes)
        er.warning ("list_approx", p.getSourceLocation ());
      buf.append ("CDATA");
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitValue (final ValuePattern p)
    {
      if (getContentType (p) == ContentType.ENUM)
      {
        buf.append ('(');
        super.visitValue (p);
        buf.append (')');
      }
      else
      {
        final Datatypes.Info info = Datatypes.getInfo (p.getDatatypeLibrary (), p.getType ());
        if (info == null)
        {
          er.warning ("unrecognized_datatype", p.getSourceLocation ());
          buf.append ("CDATA");
        }
        else
        {
          final String type = info.closestType ();
          if (warnDatatypes)
            er.warning ("value_approx", type, p.getSourceLocation ());
          buf.append (type);
        }
      }
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitChoice (final ChoicePattern p)
    {
      final ContentType t = getContentType (p);
      if (t == ContentType.ENUM)
      {
        buf.append ('(');
        nestedSimpleTypeOutput.visitChoice (p);
        buf.append (')');
      }
      else
        if (t == ContentType.SIMPLE_TYPE_CHOICE)
        {
          if (warnDatatypes)
            er.warning ("datatype_choice_approx", p.getSourceLocation ());
          buf.append ("CDATA");
        }
        else
          super.visitChoice (p);
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      final ContentType t = getContentType (p);
      if (t == ContentType.ENUM)
      {
        buf.append ('(');
        super.visitRef (p);
        buf.append (')');
      }
      else
        if (t == ContentType.TEXT)
          buf.append ("CDATA");
        else
          super.visitRef (p);
      return VoidValue.VOID;
    }

  }

  private class ValueOutput extends PatternOutput
  {
    @Override
    public VoidValue visitValue (final ValuePattern p)
    {
      buf.append ("CDATA #FIXED ");
      attributeValueLiteral (p.getValue ());
      return VoidValue.VOID;
    }

    @Override
    public VoidValue visitRef (final RefPattern p)
    {
      paramEntityRef (p);
      return VoidValue.VOID;
    }
  }

  class GrammarOutput implements ComponentVisitor <VoidValue>
  {
    public void visitContainer (final Container c)
    {
      final List <Component> list = c.getComponents ();
      for (int i = 0, len = list.size (); i < len; i++)
        (list.get (i)).accept (this);
    }

    public VoidValue visitDiv (final DivComponent c)
    {
      outputLeadingComments (c);
      outputInitialChildComments (c);
      visitContainer (c);
      outputFollowingComments (c);
      return VoidValue.VOID;
    }

    public VoidValue visitDefine (final DefineComponent c)
    {
      if (c.getName () == DefineComponent.START)
      {
        outputLeadingComments (c);
        outputFollowingComments (c);
        if (analysis.getPattern () == analysis.getGrammarPattern ())
          c.getBody ().accept (nestedContentModelOutput);
      }
      else
      {
        if (getContentType (c.getBody ()) == ContentType.DIRECT_SINGLE_ELEMENT)
          outputElement ((ElementPattern) c.getBody (), c);
        else
          if (!doneParamEntities.contains (c.getName ()))
          {
            doneParamEntities.add (c.getName ());
            outputParamEntity (c);
          }
      }
      outputQueuedElements ();
      return VoidValue.VOID;
    }

    public VoidValue visitInclude (final IncludeComponent c)
    {
      outputInclude (c);
      return VoidValue.VOID;
    }
  }

  void outputQueuedElements ()
  {
    for (int i = 0; i < elementQueue.size (); i++)
      outputElement (elementQueue.get (i), null);
    elementQueue.clear ();
  }

  static void output (final boolean warnDatatypes,
                      final Analysis analysis,
                      final OutputDirectory od,
                      final ErrorReporter er) throws IOException
  {
    try
    {
      new DtdOutput (warnDatatypes, analysis.getMainUri (), analysis, new HashSet <String> (), od, er).topLevelOutput ();
    }
    catch (final WrappedIOException e)
    {
      throw e.cause;
    }
  }

  void topLevelOutput ()
  {
    final GrammarPattern grammarPattern = analysis.getGrammarPattern ();
    xmlDecl ();
    final Pattern p = analysis.getPattern ();
    if (p != grammarPattern)
    {
      outputLeadingComments (p);
      p.accept (nestedContentModelOutput);
      outputQueuedElements ();
    }
    if (grammarPattern != null)
    {
      outputLeadingComments (grammarPattern);
      outputInitialChildComments (grammarPattern);
      grammarOutput.visitContainer (grammarPattern);

      outputFollowingComments (grammarPattern);
    }
    close ();
  }

  void subOutput (final GrammarPattern grammarPattern)
  {
    xmlDecl ();
    outputLeadingComments (grammarPattern);
    outputInitialChildComments (grammarPattern);
    grammarOutput.visitContainer (grammarPattern);
    outputFollowingComments (grammarPattern);
    close ();
  }

  void xmlDecl ()
  {
    write ("<?xml encoding=\"");
    write (encoding);
    write ("\"?>");
    outputNewline ();
  }

  ContentType getContentType (final Pattern p)
  {
    return analysis.getContentType (p);
  }

  AttributeType getAttributeType (final Pattern p)
  {
    return analysis.getAttributeType (p);
  }

  Pattern getBody (final String name)
  {
    return analysis.getBody (name);
  }

  void paramEntityRef (final RefPattern p)
  {
    final String name = p.getName ();
    buf.append ('%');
    buf.append (name);
    buf.append (';');
    requiredParamEntities.add (name);
  }

  void attributeValueLiteral (final String value)
  {
    buf.append ('\'');
    for (int i = 0, len = value.length (); i < len; i++)
    {
      final char c = value.charAt (i);
      switch (c)
      {
        case '<':
          buf.append ("&lt;");
          break;
        case '&':
          buf.append ("&amp;");
          break;
        case '\'':
          buf.append ("&apos;");
          break;
        case '"':
          buf.append ("&quot;");
          break;
        case '\r':
          buf.append ("&#xD;");
          break;
        case '\n':
          buf.append ("&#xA;");
          break;
        case '\t':
          buf.append ("&#9;");
          break;
        default:
          buf.append (c);
          break;
      }
    }
    buf.append ('\'');
  }

  void outputRequiredComponents ()
  {
    for (int i = 0; i < requiredParamEntities.size (); i++)
    {
      final String name = requiredParamEntities.get (i);
      final Component c = part.getWhereProvided (name);
      if (c == null)
        externallyRequiredParamEntities.add (name);
      else
        if (c instanceof DefineComponent)
        {
          if (!doneParamEntities.contains (name))
          {
            doneParamEntities.add (name);
            outputParamEntity ((DefineComponent) c);
          }
        }
        else
          outputInclude ((IncludeComponent) c);
    }
    requiredParamEntities.clear ();
  }

  void outputInclude (final IncludeComponent inc)
  {
    final String href = inc.getUri ();
    if (doneIncludes.contains (href))
      return;
    if (pendingIncludes.contains (href))
    {
      er.error ("sorry_include_depend", inc.getSourceLocation ());
      return;
    }
    pendingIncludes.add (href);
    final DtdOutput sub = new DtdOutput (warnDatatypes, href, analysis, reservedEntityNames, od, er);
    final GrammarPattern g = (GrammarPattern) analysis.getSchema (href);
    sub.subOutput (g);
    requiredParamEntities.addAll (sub.externallyRequiredParamEntities);
    outputRequiredComponents ();
    outputLeadingComments (inc);
    final String entityName = genEntityName (inc);
    outputNewline ();
    write ("<!ENTITY % ");
    write (entityName);
    write (" SYSTEM ");
    write ('"');
    // XXX deal with " in filename (is it allowed by URI syntax?)
    write (od.reference (sourceUri, href));
    write ('"');
    write ('>');
    outputNewline ();
    write ('%');
    write (entityName);
    write (';');
    outputNewline ();
    outputFollowingComments (inc);
    doneIncludes.add (href);
    pendingIncludes.remove (href);
  }

  String genEntityName (final IncludeComponent inc)
  {
    String entityName = getAttributeAnnotation (inc, DTD_URI, "entityName");
    if (entityName != null)
    {
      entityName = entityName.trim ();
      if (!Naming.isNcname (entityName))
      {
        er.warning ("entity_name_not_ncname", entityName, inc.getSourceLocation ());
        entityName = null;
      }
    }
    if (entityName == null)
    {
      String uri = inc.getUri ();
      final int slash = uri.lastIndexOf ('/');
      if (slash >= 0)
        uri = uri.substring (slash + 1);
      final int dot = uri.lastIndexOf ('.');
      if (dot > 0)
        uri = uri.substring (0, dot);
      if (Naming.isNcname (uri))
        entityName = uri;
    }
    if (entityName == null)
      entityName = "ent";
    if (!reserveEntityName (entityName))
    {
      for (int i = 1;; i++)
      {
        final String tem = entityName + Integer.toString (i);
        if (reserveEntityName (tem))
        {
          entityName = tem;
          break;
        }
      }
    }
    return entityName;
  }

  private boolean reserveEntityName (final String name)
  {
    if (reservedEntityNames.contains (name))
      return false;
    reservedEntityNames.add (name);
    return true;
  }

  void outputParamEntity (final DefineComponent def)
  {
    final String name = def.getName ();
    final Pattern body = def.getBody ();
    final ContentType t = getContentType (body);
    buf.setLength (0);
    boolean wrap = true;
    if (t.isA (ContentType.MODEL_GROUP) || t.isA (ContentType.NOT_ALLOWED) || t.isA (ContentType.MIXED_ELEMENT_CLASS))
      body.accept (nestedContentModelOutput);
    else
      if (t.isA (ContentType.MIXED_MODEL))
        body.accept (topLevelContentModelOutput);
      else
        if (t.isA (ContentType.EMPTY))
        {
          attributeOutput.output (body);
          wrap = false;
        }
        else
          if (t.isA (ContentType.ENUM))
            body.accept (nestedSimpleTypeOutput);
          else
            if (t.isA (ContentType.VALUE))
            {
              body.accept (valueOutput);
              wrap = false;
            }
            else
              if (t.isA (ContentType.SIMPLE_TYPE))
                body.accept (topLevelSimpleTypeOutput);
    final String replacement = buf.toString ();
    outputRequiredComponents ();
    outputLeadingComments (def);
    final String elementName = analysis.getParamEntityElementName (name);
    if (elementName != null)
    {
      if (replacement.length () > 0)
      {
        outputNewline ();
        write ("<!ATTLIST ");
        write (elementName);
        outputAttributeNamespaces (body);
        write (replacement);
        write ('>');
        outputNewline ();
      }
    }
    else
    {
      doneParamEntities.add (name);
      outputNewline ();
      final String prefix = "<!ENTITY % " + name + " \"";
      final String suffix = "\">";
      if (!wrap)
      {
        write (prefix);
        write (replacement);
        write (suffix);
        outputNewline ();
      }
      else
        outputModelBreak (prefix, replacement, suffix);
    }
    outputFollowingComments (def);
  }

  private void outputModelBreak (final String prefix, final String replacement, final String suffix)
  {
    for (final ModelBreaker breaker = new ModelBreaker (prefix, replacement, suffix, lineLength); breaker.hasNextLine ();)
    {
      write (breaker.nextLine ());
      outputNewline ();
    }
  }

  void outputElement (final ElementPattern p, final Annotated def)
  {
    buf.setLength (0);
    final Pattern content = p.getChild ();
    final ContentType ct = getContentType (content);
    if (ct == ContentType.EMPTY)
    {}
    else
      if (ct == ContentType.MIXED_ELEMENT_CLASS)
      {
        er.warning ("mixed_choice_approx", p.getSourceLocation ());
        buf.append ("(");
        content.accept (nestedContentModelOutput);
        buf.append (")*");
      }
      else
        if (ct.isA (ContentType.SIMPLE_TYPE))
        {
          if (warnDatatypes)
            er.warning ("data_content_approx", p.getSourceLocation ());
          buf.append ("(#PCDATA)");
        }
        else
          if (ct == ContentType.NOT_ALLOWED)
            return; // leave it undefined
          else
            content.accept (topLevelContentModelOutput);
    final String contentModel = buf.length () == 0 ? "EMPTY" : buf.toString ();
    buf.setLength (0);
    attributeOutput.output (content);
    final String atts = buf.toString ();
    outputRequiredComponents ();
    if (def != null)
      outputLeadingComments (def);
    outputLeadingComments (p);
    final List <NameNameClass> names = NameClassSplitter.split (p.getNameClass ());
    for (final NameNameClass name : names)
    {
      final String ns = name.getNamespaceUri ();
      String qName = name.getLocalName ();
      final String prefix = analysis.getElementPrefixForNamespaceUri (ns);
      if (prefix != null)
        qName = prefix + ":" + qName;
      outputNewline ();
      outputModelBreak ("<!ELEMENT " + qName + " ", contentModel, ">");
      boolean needXmlns;
      if (ns == NameClass.INHERIT_NS)
        needXmlns = false;
      else
        if (prefix == null)
          needXmlns = true;
        else
          needXmlns = !analysis.getAttributeNamespaces (content).contains (ns);
      if (atts.length () != 0 || needXmlns)
      {
        write ("<!ATTLIST ");
        write (qName);
        if (needXmlns)
        {
          outputNewline ();
          outputIndent ();
          if (prefix != null)
          {
            write ("xmlns:");
            write (prefix);
          }
          else
            write ("xmlns");
          write (" CDATA #FIXED ");
          buf.setLength (0);
          attributeValueLiteral (ns);
          write (buf.toString ());
        }
        if (atts.length () != 0)
          outputAttributeNamespaces (content);
        write (atts);
        write ('>');
        outputNewline ();
      }
    }
    if (def != null)
      outputFollowingComments (def);
  }

  void outputAttributeNamespaces (final Pattern p)
  {
    final Set <String> namespaces = analysis.getAttributeNamespaces (p);
    for (final String ns : namespaces)
    {
      final String prefix = analysis.getPrefixForNamespaceUri (ns);
      outputNewline ();
      outputIndent ();
      write ("xmlns:");
      write (prefix);
      write (" CDATA #FIXED ");
      buf.setLength (0);
      attributeValueLiteral (ns);
      write (buf.toString ());
    }
  }

  void outputLeadingComments (final Annotated a)
  {
    outputComments (a.getLeadingComments ());
  }

  void outputInitialChildComments (final Annotated a)
  {
    outputComments (a.getChildElementAnnotations ());
  }

  void outputFollowingComments (final Annotated a)
  {
    outputComments (a.getFollowingElementAnnotations ());
  }

  void outputComments (final List <? extends AnnotationChild> list)
  {
    for (final AnnotationChild child : list)
      if (child instanceof Comment)
        outputComment (((Comment) child).getValue ());
  }

  void outputComment (final String value)
  {
    outputNewline ();
    write ("<!--");
    int start = 0;
    for (;;)
    {
      final int i = value.indexOf ('\n', start);
      if (i < 0)
      {
        if (start == 0)
        {
          write (' ');
          write (value);
          write (' ');
        }
        else
        {
          outputNewline ();
          write (value.substring (start));
          outputNewline ();
        }
        break;
      }
      outputNewline ();
      write (value.substring (start, i));
      start = i + 1;
    }
    write ("-->");
    outputNewline ();
  }

  void outputIndent ()
  {
    for (int i = 0; i < indent; i++)
      write (' ');
  }

  void outputNewline ()
  {
    write (lineSep);
  }

  static class WrappedIOException extends RuntimeException
  {
    final IOException cause;

    WrappedIOException (final IOException cause)
    {
      this.cause = cause;
    }

    @Override
    public Throwable getCause ()
    {
      return cause;
    }
  }

  void write (final String s)
  {
    try
    {
      writer.write (s);
    }
    catch (final IOException e)
    {
      throw new WrappedIOException (e);
    }
  }

  void write (final char c)
  {
    try
    {
      writer.write (c);
    }
    catch (final IOException e)
    {
      throw new WrappedIOException (e);
    }
  }

  void close ()
  {
    try
    {
      writer.close ();
    }
    catch (final IOException e)
    {
      throw new WrappedIOException (e);
    }
  }

  private static String getDefaultValue (final AttributePattern p)
  {
    return getAttributeAnnotation (p, WellKnownNamespaces.RELAX_NG_COMPATIBILITY_ANNOTATIONS, "defaultValue");
  }

  private static String getAttributeAnnotation (final Annotated p, final String ns, final String localName)
  {
    final List <AttributeAnnotation> list = p.getAttributeAnnotations ();
    for (int i = 0, len = list.size (); i < len; i++)
    {
      final AttributeAnnotation att = list.get (i);
      if (att.getLocalName ().equals (localName) && att.getNamespaceUri ().equals (ns))
        return att.getValue ();
    }
    return null;
  }
}
